[pull] develop from duckduckgo:develop#350
Open
pull[bot] wants to merge 5667 commits intoRachelmorrell:developfrom
Open
[pull] develop from duckduckgo:develop#350pull[bot] wants to merge 5667 commits intoRachelmorrell:developfrom
pull[bot] wants to merge 5667 commits intoRachelmorrell:developfrom
Conversation
3bc8c57 to
ceb6fa4
Compare
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213130409431278 ### Description First part of the recent chats feature, which implements a chat suggestion list in the duck.ai tab of the input screen. Introduces a new feature flag: aiChatSuggestions. When enabled, the AI chat suggestions (pinned and recent chats) in the input screen's duck.ai mode. The UI is currently a work in progress and is using static test data. *Changes:* - Added a new overlay in the input screen fragment for the chat suggestions. This follows the same pattern as the autocomplete overlay. - Add a recycler view (with its adapter and items) to the chatSuggestionsOverlay to display the list of recent chats. - Modified how the Logo visibility is handled. The Duck.ai chat logo should no longer be visible even if there are not recent chat items. Only the search logo will be visible (Follows iOS implementation) - Added icons for the chats (pin vs chat bubble) - Added the model for the Chat suggestion. It follow the expected structure for future integration with the JS frontend. - Modified the Input Screen View Model to load the static list. - All the changes are UX/fragments, Testing will be done through Maestro. Unit tests will be implemented when real data starts flowing through the view model. *Next Steps* - Connect with the real frontend data once the JS webview interface is ready - Clicking on a recent chat should navigate you to the Duck.ai page and load the related chat. - Maestro tests ### Steps to test this PR Notes: in order to facilitate testing, I created an additional branch with static data. Please pull the `origin/feature/youssef/do_not_merge/recent_ai_chats_ui_test_data` branch to test this PR with data and see the behavior. This would prevent having test data part of the merge and allow PR testing. - Pull the changes and run the app - Go to feature flags settings and turn on "aiChatSuggestions" flag (it's off by default) - Go back to the input screen, make sure the omnibar is enabled - Switch to the "Duck.ai" tab. You should see a static list of chats, both pinned and recent. - Switch back and forth between search and chat to make sure the interaction and visibility is correct. You can test different behaviors on the search tab such as searching or adding a favorite. No impact should be observed. - If "aiChatSuggestions" is disabled, the app should behave as it is currently in production. No impact should be observed ### UI changes | Before | After | | ------ | ----- | ||| ||| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Feature-flagged but touches core input-screen UI state/animation and overlay interaction, which can introduce subtle regressions in tab switching, visibility, and touch handling. > > **Overview** > Adds a new remote sub-feature flag, `DuckChatFeature.aiChatSuggestions`, to gate an in-progress “recent/pinned chats” suggestions experience in the Duck.ai input-screen tab. > > When enabled, the input screen now includes a new `chatSuggestionsOverlay` (RecyclerView + bottom fade/blur) and shared overlay animation logic, and updates logo/overlay/viewpager-interaction behavior to avoid showing the Duck.ai logo and to ensure autocomplete/suggestions overlays don’t overlap during mode switches. `ChatTabFragment` is expanded to wire up a `ChatSuggestionsAdapter` and observe a new `InputScreenViewModel.chatSuggestions` flow (loading stubbed via a TODO), with new pin/chat icons and an `item_chat_suggestion` row layout; tests are updated to inject the new feature toggle dependency. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 595d297. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Youssef Keyrouz <ykeyrouz@Youssefs-MacBook-Pro.local>
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213085863198314 ### Description See attached task description ### Steps to test this PR https://app.asana.com/1/137249556945/task/1213176315984932 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches analytics/pixel APIs and their call sites, requiring signature updates across production and tests; runtime risk is mitigated by wrapping VPN state lookup in `runCatching` and defaulting to `false`. > > **Overview** > PIR now enriches eligible scan/opt-out result pixels with a new `vpn_connection_state` parameter (mapped to `connected`/`disconnected`) for `PIR_SCAN_STAGE_RESULT_*` and opt-out submit success/failure pixels. > > `RealPirRunStateHandler` is updated to depend on `NetworkProtectionState` and safely query `isRunning()` (fail-closed) when emitting these pixels; tests and the end-to-end instrumentation setup add a `FakeNetworkProtectionState`, and `pir-impl` now depends on `:network-protection-api`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 754362f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
…icons. (#7710) Task/Issue URL: https://app.asana.com/1/137249556945/task/1212341818869426 ### Description Handle unreasonably large favicons gracefully, by setting a max size in pixels. ### Steps to test this PR 1. Start a local HTTP server serving a page with a large apple-touch-icon: cd /tmp/favicon_test && python3 -m http.server 8888 (The test page serves a 6708×6708 PNG as its touch icon) 2. On the device, open the DuckDuckGo browser and navigate to http://{your-server-ip}:8888 3. Wait ~2 seconds for the touch icon to be discovered and downloaded 4. Open the tab switcher 5. Without the changes: App crashes with: `RuntimeException: Canvas: trying to draw too large(179989056bytes) bitmap` But with the changes shouldn't crash. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, localized change that only constrains favicon bitmap dimensions; low risk aside from potential quality reduction for very large icons. > > **Overview** > Prevents crashes from *unreasonably large* favicons by introducing a `MAX_FAVICON_SIZE_PX` cap (512px) and enforcing it across Glide favicon fetches. > > Both async (`CustomTarget`) and sync (`submit`) download paths for disk and URL loads now request bitmaps at the capped size, limiting memory usage during decoding/rendering. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 91ca909. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213076149941930 ### Description Fixes `Room` schema export by switching to the Room Gradle plugin. Several modules were using `annotationProcessorOptions` to configure the schema location, but this only works with `kapt`, not `KSP`. Since `Room` is compiled with `KSP` in these modules, schemas weren't being exported. Additionally (the original reason I was looking at this), absolute paths were being used which breaks `Gradle` caching. **Changes** - Added `androidx.room Gradle` plugin to affected modules - Replaced `annotationProcessorOptions` and `ksp { arg(...) }` with the new room `{ schemaDirectory(...) }` **Why the JSON schema changes/creation** Because many of the DB schema files were missing, and one was wrong. They've been missing for some time. 6 databases were affected: | Database | Missing Schemas | |-------------------------|---------------------| | AppDatabase | versions 50-60 | | VpnDatabase | versions 33-34 | | RemoteMessagingDatabase | version 2 | | SitePermissionsDatabase | versions 2-5 | | VoiceSearchDatabase | version 2 | | BrokenSiteDatabase | incorrect v1 schema | 2 databases were already working (`PirDatabase`, `Autofill` databases) because they used the correct `ksp { arg(...) }` syntax already. **Why this matters** - Enables Gradle build caching (absolute paths in old config broke cache keys) - Schema JSON files are now correctly exported for migration testing - Uses the officially recommended Room configuration method ### Steps to test this PR - Fresh install develop, do some stuff in the app then install this branch. Verify no problems in the upgrade <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Build config changes span many modules and regenerate schema artifacts; risk is mainly in build/cache behavior and Room migration test expectations rather than runtime logic. > > **Overview** > **Migrates Room schema export configuration to the official `androidx.room` Gradle plugin** across multiple modules (including `app`, `vpn-store`, `broken-site-store`, `remote-messaging-store`, `site-permissions-store`, `voice-search-store`, `experiments-impl`, `pir-impl`, and `autofill-impl`). This removes the old `annotationProcessorOptions`/`ksp { arg("room.schemaLocation", ...) }` approach, switches schema paths to project-relative (`$projectDir/schemas`), and adds `room { schemaDirectory(...) }` plus schema assets wiring for tests. > > Adds/updates Room exported schema JSONs for several databases (new versions for `AppDatabase`, `VpnDatabase`, `RemoteMessagingDatabase`, `SitePermissionsDatabase`, `VoiceSearchDatabase`) and corrects `BrokenSiteDatabase` v1 schema (primary key/identity hash), enabling migration testing and improving Gradle build cache reliability. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6b0827e. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1201870266890790/task/1210936487262413?focus=true ### Description Our other browsers handle these states similarly for the most part, so we should adapt Android to match other platforms. As it stands, we get breakage reports from Android users whenever the privacy shield is showing the red warning dot, which means we get a lot of non-actionable feedback for people's router IPs and for sites we're trying to speculatively mitigate breakage on (turning off blocking to see if it reduces reports). ### Steps to test this PR - [x] Navigate to `noaprints.com` and `marvel.com` (both have protection disabled in the config) - [x] Confirm for each that you see the normal green privacy shield state for both (though if you click into the dashboard, the fact that protections are disabled is shown & the menu item shows as "enable privacy protection" - [x] Navigate to your local network address(es) and confirm that you see the globe icon rather than the UNPROTECTED state (privacy shield with a red dot) ### UI changes | Before | After | | ------ | ----- | <img width="1080" height="2400" alt="marvelOld" src="https://github.com/user-attachments/assets/695101f5-1ac0-49d0-a885-fd9efef852f8" />|<img width="1080" height="2400" alt="marvelNew" src="https://github.com/user-attachments/assets/cb880287-3e5a-4350-8c92-cce61469dd60" />| |<img width="1080" height="2400" alt="localOld" src="https://github.com/user-attachments/assets/f1a8126b-8222-4bac-b23a-6a62afc65eb8" />|<img width="1080" height="2400" alt="localNew" src="https://github.com/user-attachments/assets/0179bc49-8096-4908-bf1d-4813af3039c2" />| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how privacy/shield and leading icon states are derived (user-visible and used by reporting), gated by a remote toggle but touching core URL classification and privacy-state logic. > > **Overview** > Standardizes omnibar leading icon/shield behavior behind a new remote feature toggle `standardizedLeadingIcon`. > > When enabled, `OmnibarLayoutViewModel` shows the **Globe** icon for localhost/private-network/file URLs (instead of the privacy shield state), and `SiteMonitor.privacyProtection()` only returns **UNPROTECTED** for *user-initiated* allowlisting (not remote-config exceptions), reducing misleading red-dot states. Adds a new `Uri.isLocalUrl` helper (IPv4/IPv6 ranges + localhost, no DNS lookups) with extensive tests, and updates constructors/tests to inject the new toggle through `SiteFactoryImpl`/`SiteMonitor`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d5bc522. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kate Manning <laghee@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213210525832193 ### Description On the input screen view, we are able to switch tabs (between Search and Duck.ai) using the swipe gesture. When we type something in the search box the autocomplete overlay is shown and a list of autocomplete suggestions is displayed. Swipe gesture works inside the list items but not in empty areas. ### Steps to reproduce - Go to Settings → AI Features → Make sure “Duck.ai” is turned on and “Search & Duck.ai” mode is selected. - Go back to the Input screen with the omnibar. The Search tab is selected by default - Swipe to the left anywhere on the page → Tab switches to Duck.ai tab correctly - Go back to the Search and type something in the chat box for the autocomplete list to show up - Swipe to the left inside the list → Tab switches correctly - Swipe to the left somewhere outside the list (in any empty area) → Tab does NOT switch. Swipe gesture is ignored. ### Root cause and fix When touching a list item, `onInterceptTouchEvent` receives ACTION_MOVE events (to allow the parent to steal it from the child). This is where horizontal swipe detection is currently implemented. When touching empty space below the items, no child handles ACTION_DOWN, so Android skips onInterceptTouchEvent for subsequent MOVE events and routes them directly to the recycler view's own onTouchEvent. onInterceptTouchEvent is never called for the ACTION_MOVE when no list item claimed the ACTION_DOWN. Fix: Added horizontal swipe detection in the recycler view's onTouchEvent to cover this path. If it's a move action, and the interceptor didn't already detect it, we detect it here. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, localized touch-handling change in a single custom `RecyclerView`; risk is limited to potential gesture-detection regressions in this view. > > **Overview** > Fixes swipe-to-switch-tabs when interacting with empty space in the autocomplete/chat suggestions overlays by adding horizontal-swipe detection to `SwipeableRecyclerView.onTouchEvent` (not just `onInterceptTouchEvent`). > > This ensures `ViewPager2` receives the gesture even when `ACTION_MOVE` events bypass interception, improving tab switching consistency during overlay display. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e425554. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Youssef Keyrouz <ykeyrouz@Youssefs-MacBook-Pro.local>
Task/Issue URL: https://app.asana.com/1/137249556945/task/1212863124420545 ### Description Adding my Asana ID to the github Asana mapping for automated task assignment to work. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Single-line update to a GitHub-to-Asana ID lookup table used by automation; no code or security-sensitive logic changes. > > **Overview** > Adds `YoussefKeyrouz` to `.github/actions/assign-release-task/github_asana_mapping.yml` so the `assign-release-task` GitHub Action can map that GitHub username to the correct Asana user ID for automated task assignment. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 192875c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Youssef Keyrouz <ykeyrouz@Youssefs-MacBook-Pro.local>
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213011616457504?focus=true ### Description Replace pillIcon images in ListItem components for the new custom view where we can set text as string ### Steps to test this PR - [ ] Install from branch - [ ] Go to Settings and check everything looks good - [ ] Go to Android Design System Preview > List Items - [ ] Check all the yellow pills there look ok ### No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches widely used design-system list-item views and their public XML attributes; risk is mainly UI regressions and attribute migration issues where old `showBetaPill`/`showNewPill`/enum `pillIcon` were referenced. > > **Overview** > **Replaces static “Beta/New” pill image usage in list-item components with the `DaxYellowPill` text-based custom view.** `OneLineListItem`, `TwoLineListItem`, and `SettingsListItem` now show/hide a `yellowPill` and set its label via new `pillText` when `pillIcon` is enabled, removing the old `showBetaPill`/`showNewPill` flags and the enum-based `pillIcon` resource mapping. > > Updates the associated XML layouts and design-system preview components to use `DaxYellowPill` and the new attrs, and removes the now-unused `showBetaPill` attribute from `WaitlistCheckListItem` attrs. Also tweaks constraints/minHeight in item layouts to accommodate the new pill view. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 068d064. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213119876946268 ### Description This PR adds translations for the Duck.ai Contextual work ### Steps to test this PR Green CI <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Changes are limited to Android string resources and a small wiring update to reference the new summarize string key, with minimal functional impact beyond displayed text. > > **Overview** > Adds localized `Duck.ai` contextual bottom-sheet strings (e.g., summarize prompt and page-content attachment/auto-send labels) across many `values-*/strings-duckchat.xml` locales and the default `values/strings-duckchat.xml`. > > Updates `DuckChatContextualFragment` to use the new `duckAIContextualPromptSummarize` string resource (replacing the previous `duckAIContextualSummarizePrompt`) and removes the old `values/donottranslate.xml` resource file; also includes minor test renaming and small SERP logo string metadata/text tweaks. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cc4ac8e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1213119876946268 --------- Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213081921435007 ### Description This PR improves the prompt replacement logic ### Steps to test this PR Enable contextualMode FF _No previous prompt_ - [x] Open contextual and tap on “Summarise This Page" - [x] Verify prompt has been replaced (no spaces before or after) _With previous prompt_ - [x] Open contextual and enter some text in the input field - [x] Tap on “Summarise This Page" - [x] Verify prompt has been added at the end of the current prompt <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Changes are limited to contextual prompt UI/state handling and add/adjust unit tests; no auth, networking, or persistence logic is materially altered. > > **Overview** > Improves Duck.ai contextual *auto-prompt* behavior by changing `replacePrompt` to append the predefined “Summarize” prompt to any existing user input (or replace it when empty), and to only show page context when the cached context JSON is valid. > > Moves prompt clearing to the ViewModel via a new `onPromptCleared` (so clearing doesn’t implicitly toggle context), updates the fragment to keep the cursor at the end when restoring a prompt, and expands/updates `DuckChatContextualViewModelTest` coverage for these cases. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 66a629c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1212015278241917/task/1212011586318109?focus=true ### Description Display the about:blank title for about blank tab ### Steps to test this PR _Check about:blank tab name_ - [x] Open the application - [x] Type "about:blank" in the address bar and submit - [x] Check your page is blank - [x] Go to tab screen - [x] Check the title of the tab is empty (not covered by this task) - [x] Open a new tab and enter a website with its own favicon - [x] Go to tab screen - [x] Check the title of the new tab, should have a title corresponding to the website - [x] Open a new Duck.ai tab - [x] Go to tab screen - [x] Check the title of the tab "Duck.ai" with Dax as favicon - [x] Open `http://privacy-test-pages.site/security/address-bar-spoofing/spoof-about-blank-emptyaddress.html` - [x] Click on "Run" button - [x] Check a blank page has been opened - [x] Go to tab screen - [x] Check the title of the tab `about:blank` and the icon is a globe ### UI changes | Before | After | | ------ | ----- | <img width="270" height="600" alt="image" src="https://github.com/user-attachments/assets/9fd438d8-adb6-49ad-a70b-6c6da32ecc54" /> | <img width="270" height="600" alt="image" src="https://github.com/user-attachments/assets/5b401c61-bc59-4e2a-a181-1915ac46ff8f" />
Task/Issue URL: https://app.asana.com/1/137249556945/project/1210856607616307/task/1213179366895625 ### Description Fix pixel definition <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Schema-only change to an analytics/pixel definition with minimal functional impact; main risk is downstream validation/consumers expecting the old shape. > > **Overview** > Updates the `m_appearance_settings_is_tracker_count_in_address_bar_toggled` pixel definition in `appearance_settings.json5` to include a new `parameters` entry, `is_enabled`, capturing whether the setting is enabled or disabled after the toggle. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 98ccc9d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1212608036467427/task/1212999357864763?focus=true ### Description Introduce a feature flag to control whether we disable the tracker animation upon launching the browser. This works by setting the prevoiusUrl to the current url being loaded, which would effectively stop the animation from playing. An edge case of launching the app from an external link should be handled via the isExternal flag already existing in the viewModel. However, there was a race-condition in how the `isExternal` flag was being [evaluated](https://github.com/duckduckgo/Android/blob/df6e0a43589aed21d44cabbbc9e8a0341185a73e/app/src/main/java/com/duckduckgo/app/browser/tabs/adapter/TabPagerAdapter.kt#L72). This is problematic on 2 different dimensions: 1- Tab loading order will affect which tab is reading isExternal from the latest intent. 2- Reading wrong intent as sometimes the correct intent to read is a deferred intent stored in [lastIntent](https://github.com/duckduckgo/Android/blob/df6e0a43589aed21d44cabbbc9e8a0341185a73e/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt#L478). I made minimal changes to fix how the isExternal is being read by storing the intended value for isExternal in a <tabId to isExternal map>, and only reading that once upon creation of the fragment. The reason why this shouldn't be in the tabRepository is that isExternal should only be used upon creation of the tab for the first time only. If for example the user relaunches the app, isExternal for an old tab should be set to false. ### Steps to test this PR - Open app - Enter a new website. - observer tracker animation. - close app and make sure process is killed. - start app. - observe same website loading but without tracker animation. ### UI changes | Before | After | | ------ | ----- | | <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches tab creation/selection flow and external-intent handling, which can affect navigation behavior and first-load experience across multiple tabs. Changes are scoped and feature-flagged, but regressions could show up as incorrect external-tab treatment or missing/extra animations. > > **Overview** > Adds a new remote-config toggle `disableTrackerAnimationOnRestart` and a one-shot `suppressTrackerAnimation` path that, on app restart, sets `BrowserTabViewModel.previousUrl` to the initial URL to prevent the tracker animation from playing. > > Refactors how *external launches* are determined by tracking external status per `tabId` in `BrowserActivity` and consuming it when creating/selecting tab fragments, avoiding races from reading the latest `Intent`. Updates fragment/tab creation APIs to pass `suppressTrackerAnimation`, adds debug logging, and extends `BrowserTabViewModelTest` to cover the new `previousUrl` behavior and guardrails (feature flag, external tabs, null URL). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2557255. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213218457009900 ### Description Add platform parameter to VPN activation pixel ### Steps to test this PR - [x] Apply patch on https://app.asana.com/1/137249556945/task/1210448620621729 - [x] Fresh install - [x] Purchase a test subscription (Free Trial) - [x] Before it expires activate VPN - [x] Check in logcat that `subscription_free_trial_vpn_activation` pixel is fired with the new `platform` parameter ### No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, localized analytics/pixel schema change; main risk is missing/incorrect `platform` values causing pixel schema mismatches or downstream reporting issues. > > **Overview** > Adds a new `activation_platform` parameter to the `subscription_free_trial_vpn_activation_u` pixel definition (enum: `apple`/`google`/`stripe`). > > Updates `SubscriptionPixelSender.reportFreeTrialVpnActivation` to require and send this platform value, and wires `FreeTrialConversionWideEvent` to pass `subscription.platform`; associated unit tests are updated to assert the new argument and non-invocation signature. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0d48361. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213074060364697 ### Description This PR ensures that when attaching context in the FE side we are properly showing the favicon ### Steps to test this PR Enable contextualMode _Feature_ - [x] Open app and open a contextual chat - [x] Send a prompt, but don’t attach the context - [x] When in Duck.ai, tap on Attach Page Content - [x] Verify context is added, and favicon too. ### UI changes | Before | After | | ------ | ----- | <img width="1080" height="2400" alt="Screenshot_20260127_202941" src="https://github.com/user-attachments/assets/18b6f145-68cf-4996-bd75-aedaa28ce090" />|<img width="1080" height="2424" alt="Screenshot_20260209_224902" src="https://github.com/user-attachments/assets/8903ddd2-7179-4a6e-bbca-7503982f6194" />| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches JS-bridge payload generation and adds bitmap-to-base64 encoding plus disk favicon lookup, which could affect performance/memory and the shape of data sent to the webview. > > **Overview** > Duck.ai contextual mode now passes the current `tabId` through JS callback handling and, when responding to `getAIChatPageContext`, looks up the tab/url favicon on disk and injects it into the returned `pageContext` as a `favicon` array containing a `data:image/png;base64,...` icon. > > This updates the contextual sheet to store/forward `sheetTabId`, extends `DuckChatJSHelper.processJsCallbackMessage` with a `tabId` parameter, and adjusts unit/instrumentation tests to cover the new argument and favicon enrichment behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 76bd669. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1213074060364697
…in state (#7722) Task/Issue URL: https://app.asana.com/1/137249556945/task/1213228272275032 ### Description Updates SetUpSyncHandler validation so the "sync already on" error is only returned for sendToSetupSync; sendToSyncSettings now proceeds to open Sync settings regardless of sign-in state. ### Steps to test this PR - QA optional - [ ] Enable internal duck ai chat state: [Internal Testing Chat Sync Integration with Native Apps](https://app.asana.com/1/137249556945/task/1212472824864986?focus=true) - [ ] Enable sync - [ ] Visit duck ai chat settings and click on the `Manage` button under `Sync & Backup`; verify sync settings activity is launched <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, well-tested change to JS message validation/gating that only affects whether the Sync settings activity is launched. > > **Overview** > Updates `SetUpSyncHandler` validation so the **"sync already on"** error is only returned for `sendToSetupSync`; `sendToSyncSettings` now proceeds to open Sync settings regardless of sign-in state. > > Refactors method names into constants and expands tests to cover the new behavior (error when feature disabled for both methods, and activity launch for `sendToSyncSettings` when already signed in). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cafd87e. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213226424542893 ### Description Update incorrect string in PIR initial scan started notification ### Steps to test this PR QA_optional <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > String-only UI copy change with no functional or data-handling impact; risk is limited to messaging/consistency. > > **Overview** > Updates the PIR foreground scan notification copy by changing `pirNotificationMessageInProgress` from “A scan is currently in progress.” to “Your scan is in progress…”, improving feedback when an initial scan starts. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 29c1d78. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213213117376953 ### Description This PR adds a new destructive secondary button type. ### Steps to test this PR - [ ] Go to Settings -> Android Design System Preview - [ ] Tap on the Buttons tap and scroll down - [ ] Verify the new secondary destructive button type is visible and looks as expected ### UI changes | Light | Dark | | ------ | ----- | || --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1213213117376953 - https://app.asana.com/0/0/1213213535164007 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk, mostly additive UI/theming resources and a new `DaxButton` subclass plus wiring in previews. Main risk is minor visual regressions or lint rule false positives/negatives due to the updated `DaxButtonStylingDetector` class list. > > **Overview** > Adds a new **destructive secondary** button variant to the design system: a `DaxButtonDestructiveSecondary` view, a new `ButtonType.DESTRUCTIVE_SECONDARY`, and theme/attribute wiring (`daxButtonDestructiveSecondary`) with dedicated stroke/text/ripple selectors and widget style. > > Updates the design-system preview layout to showcase the new button in small/large, icon, and disabled states, and extends the custom lint rule (`DaxButtonStylingDetector`) plus its tests to recognize the new button class (and updated `com.duckduckgo.common.ui.view.button.*` class names). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 1371e45. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213210377163129 ### Description * Don't try to open context menu until bookmark added botomSheet has disappeared ### Steps to test this PR _Feature 1_ - [ ] [Release tests](https://app.maestro.dev/project/proj_01htg54rdtfwx8rgbzv03cxkpf/maestro-test/app/app_01hkqhj1thevwtn9ym8a2ctn2r/upload/mupload_01kh6eembtfm6vpmdhhfptas7c?sort=name) are passing on Maestro ### UI changes n/a <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Test-only timing change; no production code or data flows are modified, with minimal risk beyond potentially masking a real UI hang if the prompt never dismisses. > > **Overview** > Stabilizes the Maestro release tests for adding/removing favorites from bookmarks by inserting an `extendedWaitUntil` step after tapping **add bookmark**. > > Both `favorites_bookmarks_add.yaml` and `favorites_bookmarks_delete.yaml` now wait up to 5s for the "Bookmark added" bottom-sheet/prompt to disappear before reopening the menu and continuing, reducing flakiness from UI timing/race conditions. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 45dd0d9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1212358379355435 ### Description This PR adds pixels to all agreed entry points ### Steps to test this PR See task <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Primarily adds telemetry events and minor method renames; risk is limited to potentially incorrect/duplicated pixel firing or slightly altered context-validation behavior. > > **Overview** > Adds **new Duck.ai Contextual-mode telemetry** definitions and wiring, introducing count + daily pixels for contextual sheet open/dismiss/expand, session restoration, new-chat, summarize quick action, page-context placeholder shown/tapped, page-context attach/remove (native + frontend), prompt submission with/without context, and invalid/empty context collection. > > Hooks these pixels into the contextual UI flow (`DuckChatContextualViewModel`/`Fragment`) and JS bridge (`RealDuckChatJSHelper` adds `togglePageContextTelemetry`), and extends daily reporting to include the automatic page-context setting state plus firing enable/disable pixels when the setting is toggled. Tests are updated/added to verify the new pixel emissions and the expanded allowed JS method list. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c32b5b8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213218785316169 ### Description This PR fixes the issue where sending a pixel shortly after enabling VPN may fail. ### Steps to test this PR - [x] Apply patch on https://app.asana.com/1/137249556945/task/1210448620621729 - [x] Fresh install - [x] Purchase a test subscription (Free Trial) - [x] Before it expires activate VPN - [x] Check in logcat that `subscription_free_trial_vpn_activation` pixel is fired with the new `platform` parameter ### No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, localized change to pixel delivery behavior; risk is limited to analytics event timing/delivery for pixels using the new `enqueue` flag. > > **Overview** > Adds an `enqueue` flag to `SubscriptionPixel` and updates `SubscriptionPixelSenderImpl.fire` to *conditionally* send pixels via `pixelSender.enqueueFire` instead of immediate `fire`. > > Marks `FREE_TRIAL_VPN_ACTIVATION` to use the queued send path, reducing the chance the pixel is lost when VPN is enabled and network conditions are transient. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit daeb405. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211724162604201/task/1211938369145727?focus=true ### Description This PR adds the integration test for the request blocklist. The test runs on https://privacy-test-pages.site/privacy-protections/request-blocklist/ and checks that all the necessary requests are blocked/ loaded. ### Steps to test this PR - [x] CI passes - [x] RequestBlocklistTest passes ### UI changes None. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) is generating a summary for commit 04e9941. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1212827841082637 ### Description Adds support for adwall (aggregate) counting pixels via web telemetry. ### Steps to test this PR Testing steps are covered in https://github.com/duckduckgo/ddg-workflow/blob/gd-detector-telemetry/technical-designs/web-detection-framework/eventhub-android-testing-plan.md <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new backgrounded/foregrounded lifecycle-driven telemetry pipeline that persists state in Room and fires pixels on timers, which could impact analytics correctness and scheduling behavior. The feature is gated by a remote toggle but touches WebView messaging and app lifecycle hooks. > > **Overview** > Introduces a new **`eventHub` telemetry feature** that ingests `webEvents`/`webEvent` messages from Content Scope Scripts, aggregates counter-based metrics over configurable periods, and fires bucketed pixels with an `attributionPeriod` parameter. > > Adds a new `event-hub-impl` module with remote-config parsing, per-pixel state persistence (Room), deduping per `webViewId`/navigation, scheduling to fire at period end, and plugins/hooks for privacy-config updates and app lifecycle foreground/background transitions. Updates content-scope messaging to inject `nativeData.webViewId` into `webEvent` messages, wires the module into `app/build.gradle`, and adds pixel definitions for daily/weekly adwall detection telemetry. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d880c78. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…ixes (#7872) Task/Issue URL: https://app.asana.com/1/137249556945/project/1198194956794324/task/1213548840330684?focus=true ### Description Three fixes to `GlobalActivityStarter`: **1. `startForResult()` overloads** The previous pattern for launching an activity for result was `startIntent() + launcher.launch(intent)`. `startIntent()` returns `Intent?` — passing `null` to `launcher.launch()` crashes at runtime with no clear error. The new `startForResult(context, params, launcher)` overloads resolve this and make the intent clearer at the call site. Before: ```kotlin val intent = globalActivityStarter.startIntent(this, SomeActivityParams) someLauncher.launch(intent) // crashes if intent is null ``` After: ```kotlin globalActivityStarter.startForResult(this, SomeActivityParams, someLauncher) ``` **2. Automatic `FLAG_ACTIVITY_NEW_TASK` for non-Activity contexts** `startIntent()` and `start()` now automatically add `FLAG_ACTIVITY_NEW_TASK` when `context !is Activity`. Callers in Services, broadcast receivers, and JS message handlers no longer need to add the flag manually after calling `startIntent()`. Before: ```kotlin val intent = globalActivityStarter.startIntent(context, DuckChatNativeSettingsNoParams) intent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK context.startActivity(intent) ``` After: ```kotlin globalActivityStarter.start(context, DuckChatNativeSettingsNoParams) ``` **3. Defensive logging** - `logcat(ERROR)` is emitted before `IllegalArgumentException` when no mapper is found for a params type, making registration bugs visible in logcat before the crash. - `logcat(WARN)` is emitted when multiple mappers claim the same params type (previously the first match silently won with no diagnostic output). Updated callers: `RestoreSubscriptionActivity` (migrated to `startForResult`), `OpenNativeSettingsHandler` (removed manual flag), `BookmarksActivity`/`BookmarksViewModel` (removed unused `LaunchSyncSettings` + `syncActivityLauncher` — the result callback only re-ran promotion eligibility, which already happens via other paths). ### Steps to test this PR - [ ] Launch an activity for result using `startForResult()` — confirm it launches correctly and the result callback fires - [ ] Restore a subscription via **Settings → Subscription → Restore** — confirm the restore flow launches and completes without a crash - [ ] Open a Duck.ai chat, trigger the native settings from the SERP settings JS handler — confirm it opens without a crash and without needing `FLAG_ACTIVITY_NEW_TASK` manually - [ ] Confirm no regression in Bookmarks — open bookmarks, verify sync promotion behaviour is unchanged - [ ] Run `./gradlew :navigation-impl:testDebugUnitTest` — all tests should pass --- > [!NOTE] > **Medium Risk** > Touches shared navigation infrastructure used across modules; intent flagging and mapper selection changes could affect activity launches if edge cases exist, though covered by new unit tests. > > **Overview** > Improves `GlobalActivityStarter` to be safer and more ergonomic: adds `startForResult(...)` overloads for `ActivityResultLauncher`, and centralizes intent construction with clearer error logging when no mapper is found. > > `start()`/`startIntent()` now automatically apply `FLAG_ACTIVITY_NEW_TASK` for non-`Activity` contexts, and log a warning when multiple mappers match the same params (first match still wins). Call sites are updated to use the new APIs (e.g. subscriptions restore flow and SERP native-settings handler), and bookmarks removes a now-unused sync-settings launch path. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8d5ce61. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1213377943508982?focus=true ### Description Updates privacy pro references inside PIR ### Steps to test this PR N/A ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > String-only changes across many locales, plus an Android manifest label tweak for `PirActivity`; risk is limited to UI text/regional resource correctness and potential missing/unused string references. > > **Overview** > Updates localized UI copy to shift messaging from *Privacy Pro* to *DuckDuckGo Subscription* in the Personal Information Removal (PIR) flow, including changing `PirActivity`’s manifest label to `@string/ddg_subscription`. > > Adds new (currently English, `translatable="false"`) onboarding and Duck.ai input-screen strings across multiple locales (e.g., Duck.ai-specific pre-onboarding titles/buttons, comparison-chart item, and updated “search vs Duck.ai” preference labels). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 764f910. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1202552961248957/task/1213634576464400 ### Description The `MetricsPixelNumericValueDetector` lint rule was silently missing violations when `MetricsPixel` was used in modules where the class is loaded from a compiled dependency JAR — the constructor `PsiMethod` can't be resolved in that case, so `visitConstructor` never fired. This PR adds a fallback detection path via `getApplicableUastTypes`/`createUastHandler` that triggers on all `UCallExpression` nodes and uses source text matching + return type filtering to identify `MetricsPixel` calls. Named-argument lookup was also reworked to use `UNamedExpression` with a Kotlin PSI fallback, making it robust regardless of argument order. All tests now skip `TestMode.REORDER_ARGUMENTS` to suppress a known lint test infrastructure warning caused by overlapping edits when positional args are nested inside outer named args. ### Steps to test this PR _MetricsPixelNumericValueDetector_ - [x] Change `SearchMetricPixelsPlugin` so one of them has a value that's not a number, i.e. "test" - [x] Run `./gradlew :feature-toggles-impl:lint` - [x] Lint should throw an error ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes lint detection logic to rely on UAST/PSI fallbacks and source-text matching, which could introduce false positives/negatives across Kotlin call shapes. > > **Overview** > Fixes `MetricsPixelNumericValueDetector` missing violations when `MetricsPixel` comes from a compiled dependency by adding a fallback scan over all `UCallExpression`s and filtering to `MetricsPixel` calls when the constructor can’t be resolved. > > Reworks argument extraction to be robust to named/out-of-order arguments by using `UNamedExpression` with a Kotlin PSI fallback, and improves error location selection by reporting on the `value` expression when possible. > > Updates tests to reflect the real `MetricsPixel` signature (default `type`), adds coverage for out-of-order named arguments, and skips `TestMode.REORDER_ARGUMENTS` to avoid test infra issues. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 693bca6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1213641986703613?focus=true ### Description Introduces new FF to enabled/disable pro entitlements fetcher Decouples that logic from the actual feature ### Steps to test this PR _Feature 1_ - [x] Apply staging patch from https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true - [x] fresh install - [x] Skip onboarding - [x] Subscription settings are visible - [x] Enter purchase flow -> ensure you see "See all Plans" - [x] don't continue, navigate back - [x] Go to feature flags inventory and disable `allowProTierPuchase` - [x] Go back to settings and enter purchase flow -> should only allow plus plans ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes subscription feature/entitlement fetching logic at app startup to be controlled by a new remote flag, which could affect which base plans are queried and cached. Impact is limited in scope but touches subscription availability behavior. > > **Overview** > Decouples pro-tier entitlements refresh from purchase availability by introducing a new `privacyPro.fetchProTierEntitlements` remote flag (default **enabled**). > > `SubscriptionFeaturesFetcher` now uses this new flag (instead of `allowProTierPurchase`) to decide whether to include both Basic and Advanced subscription products when selecting base plans to fetch and cache subscription features/entitlements. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5d1bab7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1213623825931245?focus=true ### Description Add condition to show promo onboarding as soon as it is available in new tab page ### Steps to test this PR - [ ] Apply patch pinned on https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true _Toggle visible_ - [x] Fresh install - [x] Skip onboarding tapping on "I've been here before" > "Start browsing" - [x] Close/open the app until you see Subscription option on Settings screen - [x] Check duck ai toggle is enabled and visible - [x] Background the app and set the date to 7 days from today. - [x] Go back to app - [x] Duck.ai toggle is visible with new tab page without the onboarding promo dialog - [x] Open a new tab - [x] Check the onboarding dialog shows correctly _Toggle no visible_ - [x] Fresh install - [x] Skip onboarding tapping on "I've been here before" > "Start browsing" - [x] Close/open the app until you see Subscription option on Settings screen - [x] Check duck ai toggle is enabled but not visible (only full screen new tab page) - [x] Background the app and set the date to 7 days from today. - [x] Go back to app - [x] Check duck.ai toggle is not launched and the onboarding dialog shows correctly ### No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low-risk logic change that only gates the New Tab Page onboarding-complete flag when the promo onboarding dialog is eligible to show; main risk is unintended onboarding/promo dialog visibility changes. > > **Overview** > Updates `BrowserTabViewModel.refreshCta` so `isOnboardingCompleteInNewTabPage` is **false** while the promo onboarding dialog is showing/eligible, allowing the New Tab Page to display the promo onboarding UI. > > Adds a unit test covering the scenario where the Privacy Pro promo CTA is returned and the onboarding-complete flag must remain unset. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a8442f9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1202552961248957/task/1213647284656734 ### Description When the `uiLockChanged` JS callback fires, the `locked` value should be forced to `false` if the current URL is from `duck.ai`. This is a temporary fix to avoid the omnibar being hidden in some scenarios. ### Steps to test this PR _Browser UI Lock — duck.ai behaviour_ - [ ] Open a duck.ai page and trigger a `uiLockChanged` JS callback with `locked: true` — verify the UI lock does **not** activate - [ ] Open a non-duck.ai page and trigger a `uiLockChanged` JS callback with `locked: true` — verify the UI lock **does** activate - [ ] Verify that disabling the `browserUiLock` feature flag prevents the command from being issued entirely |(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) is generating a summary for commit 18e3f23. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
- Automated content scope scripts dependency update This PR updates the content scope scripts dependency to the latest available version and copies the necessary files. Tests will only run if something has changed in the `node_modules/@duckduckgo/content-scope-scripts` folder. If only the package version has changed, there is no need to run the tests. If tests have failed, see https://app.asana.com/0/1202561462274611/1203986899650836/f for further information on what to do next. _`content-scope-scripts` folder update_ - [x] All tests must pass - [x] Privacy tests must pass _Only `content-scope-scripts` package update_ - [ ] All tests must pass - [ ] Privacy tests do not need to run Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1213634152051167?focus=true ### Description See attached task description ### Steps to test this PR Smoke test PIR broker json download <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the broker-data download/extraction pipeline and adds ZIP path validation; issues here could prevent broker updates or drop files unexpectedly, but impact is contained to PIR updates. > > **Overview** > **Hardens PIR broker JSON updates** by downloading the broker ZIP to a temp file in `cacheDir`, extracting only `.json` entries, and always cleaning up temp/extract folders. > > **Improves safety and robustness**: adds ZIP-slip protection via canonical-path checks, reuses a single lazy Moshi `brokerAdapter`, streams JSON parsing with okio instead of `readText()`, and ensures coroutine cancellation is rethrown. Adds a unit test verifying malicious ZIP entries are skipped and updates existing tests to provide `cacheDir`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 31d1c20. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213610121584461 ### Description When a user accepts Duck.ai terms and conditions and had already accepted them previously, fire a pixel to track this duplicate acceptance. Two separate pixels are sent depending on whether Sync is enabled: - `m_aichat_terms_accepted_duplicate_sync_on` - `m_aichat_terms_accepted_duplicate_sync_off` A new `DuckChatTermsOfServiceHandler` class encapsulates this logic, keeping it out of the already-large `RealDuckChatJSHelper`. The acceptance state is persisted in DataStore via a new `DUCK_AI_TERMS_ACCEPTED` boolean key. ### Steps to test this PR _Duplicate T&C acceptance pixel_ - [x] Open Duck.ai and accept terms and conditions - [ ] Close and reopen Duck.ai, accept terms again - [ ] Verify the appropriate pixel is fired (`sync_on` or `sync_off` depending on Sync state) - [ ] Verify first-time acceptance does not fire a pixel ### UI changes None <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: adds a new persisted boolean flag and additional pixel firing paths without changing core chat or sync behavior. > > **Overview** > Adds a new `USER_DID_ACCEPT_TERMS_AND_CONDITIONS` report metric and JS messaging method (`userDidAcceptTermsAndConditions`) to signal when the user accepts Duck.ai terms. > > On terms acceptance, `RealDuckChatPixels` now persists a `DUCK_AI_TERMS_ACCEPTED` flag and fires a base acceptance pixel, plus an additional **duplicate-acceptance** pixel when the flag was already set (`DUCK_CHAT_TERMS_ACCEPTED_DUPLICATE_SYNC_ON` / `_SYNC_OFF` depending on Sync state) via a new `DuckChatTermsOfServiceHandler` abstraction. > > Extends datastore and unit tests to cover the new persistence and pixel behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5fbdeb6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Josh Leibstein <joshliebe@gmail.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1212015278241917/task/1211670072973939?focus=true ### Description Create DaxPageHeader component in Compose for settings screens. ### Steps to test this PR _Check DaxPageHeader_ - [ ] Open the application - [ ] Go to the ADS screens from developer settings - [ ] Open the "Templates" tab - [ ] Check PageHeaders component follow our actual XML implementation ### UI changes <img width="270" height="600" alt="image" src="https://github.com/user-attachments/assets/01b4ea73-fafe-4721-a9cd-4ef47e946e38" />
Task/Issue URL: https://app.asana.com/1/137249556945/project/1198194956794324/task/1213651965660200?focus=true ### Description Updates `url-predictor-android` from `0.3.13` to `0.3.15`. This version includes a fix for embedded newlines and tab characters in omnibar input being misclassified as a Navigate decision (triggering navigation) instead of a Search decision (triggering a search query). ### Steps to test this PR _Test_ - [x] In toggle input screen, select duck.ai input - [x] Input `https://example.com` + newline + `test` into the omnibar. It should open a DDG search, **not** navigate to example.com - [x] Paste text with an embedded newline where the URL comes second, e.g. `hello world` on line 1 and `https://example.com` on line 2 — should open a DDG search, not navigate - [ ] Paste text with a tab character followed by something URL-like, e.g. a tab then `example.com` — should open a DDG search, not navigate _Regression — normal navigation still works_ - [x] Type a plain URL in the omnibar (e.g. `duckduckgo.com`) and confirm it navigates to the site - [x] Type a full URL with scheme (e.g. `https://wikipedia.org`) and confirm it navigates directly - [x] Type a search query (e.g. `how do penguins sleep`) and confirm it opens a DDG search results page ### UI changes N/A — no UI changes.
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211654189969294/task/1213651299034811 ### Description Adds a mic button on the duck.ai tab of the input screen that opens duck.ai directly in voice mode (`duck.ai/?mode=voice-mode`), giving users 1-click access to voice chat. - New `duckAiVoiceEntryPoint` sub-feature flag (`DefaultFeatureValue.INTERNAL` — off in production, on in internal builds) - New `openVoiceDuckChat()` on the `DuckChat` API → implemented in `RealDuckChat` by appending `mode=voice-mode` and forcing a new session - `InputScreenViewModel`: extended the voice button visibility `combine` block to be tab-aware; duck.ai tab + flag on → button follows `voiceInputAllowed` (text presence) rather than `VoiceSearchAvailability` - `InputScreenFragment`: both voice click handlers route to `viewModel.onVoiceEntryTapped()` on the duck.ai tab when flag is on - Pixel: `m_aichat_voice_entry_tapped` fired on tap - 7 new unit tests in `InputScreenViewModelTest` ### Steps to test this PR _Enable flag_ - [x] In internal build, enable `duckAiVoiceEntryPoint` under duck.ai feature flags _Duck.ai tab — empty field_ - [x] Open the input screen and switch to the duck.ai tab - [x] Mic button is visible (even if private voice search is disabled in settings) - [x] Tap the mic button → duck.ai opens with `?mode=voice-mode` in the URL, fresh session _Duck.ai tab — with text_ - [x] Type something in the input field → mic button disappears, send button appears - [x] Clear the field → mic button reappears _Search tab (unchanged)_ - [x] Switch to the search tab → mic button follows voice search availability as before - [x] Tap mic → system voice recognition launches (not duck.ai voice mode) _Flag off_ - [x] Disable `duckAiVoiceEntryPoint` → duck.ai tab mic button behaves as before (follows voice search availability, launches system voice recognition) ### UI changes | Before | After | | ------ | ----- | |(Upload before screenshot)|(Upload after screenshot)| --- > [!NOTE] > **Medium Risk** > Changes input-screen voice button behavior behind a new feature flag and adds a new DuckChat navigation path that forces a fresh session; risk is moderate due to UI/flow branching and URL/session handling. > > **Overview** > Adds a new, flag-gated *Duck.ai voice entry point* on the input screen: when on the Duck.ai tab and `duckAiVoiceEntryPoint` is enabled, the mic button opens Duck.ai with `mode=voice-mode` (forcing a new session) instead of launching system voice search. > > Introduces `DuckChat.openVoiceDuckChat()` (implemented in `RealDuckChat` via `mode=voice-mode` query param), updates voice button visibility logic in `InputScreenViewModel` to be tab-aware, and wires both voice click handlers in `InputScreenFragment` to the new behavior. Adds new pixels (`m_aichat_voice_entry_tapped_*`) plus a shared `fireCountAndDaily` helper, and expands unit test coverage for the new routing/visibility/session semantics. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0ed7e9a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671518894266/task/1213642299611483?focus=true ### Description Instead of skipping the workflow at the trigger level, the workflow now always runs but gates all jobs behind a check_changes job that inspects the diff. - If only `.md` or `.github/` files changed → all jobs are skipped (GitHub treats skipped as passing for branch protection) - If `ci.yml` itself changed → all jobs run normally, so regressions are caught before merging - If any code file changed → all jobs run normally ### Steps to test this PR QA optional: - Open a PR that only modifies an `.md` file → check_changes should pass, all other jobs should be skipped, PR should be mergeable - Open a PR that only modifies a `.github` workflow file (not `ci.yml`) → check_changes should pass, all other jobs should be skipped, PR should be mergeable - Open a PR that modifies `.github/workflows/ci.yml` → all jobs should run - Open a PR that modifies any source file → all jobs should run I tested all of these via #7962. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Modifies the CI workflow execution logic to conditionally skip most jobs based on diff contents, which could unintentionally reduce coverage if the change-detection logic is wrong. > > **Overview** > The CI workflow no longer uses trigger-level `paths-ignore`; instead it always starts and runs a new `check_changes` job that diffs the PR (or always allows `push`/`workflow_dispatch`) to decide whether checks should run. > > All existing jobs (`code_formatting`, `unit_tests`, `lint`, `android_tests`) now `need` `check_changes` and are gated by `if: needs.check_changes.outputs.should_run == 'true'`, so docs-only or non-`ci.yml` `.github/` changes skip the expensive checks while changes to `ci.yml` or any code still run the full suite. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9ff5ad8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
#7955) Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213635828556477?focus=true ### Description Pins the version of the gradler-profiler to v0.23.0 to stabilize our metrics. Refs gradle/gradle-profiler#739. ### Steps to test this PR No QA needed. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk CI workflow change that only affects the nightly benchmark job by making the `gradleprofiler` install deterministic. > > **Overview** > Pins the GitHub Actions nightly build benchmark workflow to install `gradleprofiler` version `0.23.0` via SDKMAN instead of the latest version, to stabilize benchmark results. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit df240fc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213690753964717?focus=true ### Description Don't show subscription dialog for subscribers ### Steps to test this PR _Pre steps_ - [x] Apply patch on https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true _Returning users who skip onboarding_ - [x] Fresh install - [x] Skip onboarding as a returning user (Skip onboarding as a returning user (I've been here before > Start Browsing) - [x] Go to Feature Flag Inventory and enable `privacyProCtaSkippedOnboarding` - [x] Purchase a test subscription - [x] Close the app - [x] Change the date in your device for >=7 days after today - [x] Open the app and check subscription onboarding dialog doesn't appear in a new tab page _Regular onboarding_ - [x] Set the device time back to normal - [x] Fresh install - [x] Don't skip onboarding and go to browser - [x] Purchase a test subscription - [x] Go through onboarding and check Subscription onboarding dialog doesn't appear after 'end onboarding dialog' in new tab page. ### No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes onboarding CTA eligibility logic to depend on subscription status, which can affect when onboarding is considered complete and whether promo dialogs appear. Risk is moderate due to potential edge cases if `SubscriptionStatus` is incorrect or delayed. > > **Overview** > Prevents the Privacy Pro onboarding CTA/dialog from showing to users who already have an active subscription by additionally gating CTA availability on `subscriptions.getSubscriptionStatus()` (only show when status is `UNKNOWN`). > > Updates onboarding completion/required CTA logic and adjusts/adds unit tests to cover subscribed vs unsubscribed scenarios and to stub the new subscription-status dependency. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9e0b7dd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
…t from the privacy-config module to the new one (#7967) Task/Issue URL: https://app.asana.com/1/137249556945/project/1211724162604201/task/1213642872484684?focus=true ### Description Moved the `RequestBlocklist` interface and implementation from the `privacy-config` module to a new dedicated `request-interception` module. Updated the `RequestBlocklist.containedInBlocklist()` method signature to accept `Uri` parameters instead of `String` parameters, improving type safety and eliminating the need for string-based domain extraction. ### Steps to test this PR _Request Blocking Functionality_ - [x] Verify that request blocking still works correctly in the browser: https://privacy-test-pages.site/privacy-protections/request-blocklist/ - [x] CI passes - [x] com.duckduckgo.espresso.RequestBlocklistTest passes ### UI changes No UI changes - internal refactoring only <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it relocates request-blocking logic into a new module and changes `RequestBlocklist` to use `Uri` parameters, which could alter matching behavior or wiring if any call sites weren’t updated. > > **Overview** > Moves `RequestBlocklist` out of `privacy-config` into a new `request-interception` API/impl module, and wires the app to depend on it. > > Updates the `containedInBlocklist` API (and call sites/tests) to accept `Uri` instead of `String`, and adjusts `WebViewRequestInterceptor` and the blocklist implementation to use host-based matching (`baseHost`) rather than string domain extraction. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 680d0fe. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1213720960821640?focus=true ### Description - Catches `IllegalArgumentException` when `dismiss` is called on a destroyed view (after a delay). ### Steps to test this PR - [ ] Visit a site - [ ] Tap the overflow menu and add a bookmark - [ ] Verify that the confirmation dialog is dismissed after a delay <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI-only change that adds defensive error handling around dialog dismissal to avoid a crash when the window is already gone. > > **Overview** > Prevents a crash in `BookmarkAddedConfirmationDialog` when the auto-dismiss timer fires after the dialog window has already been removed. > > The auto-dismiss `dismiss()` call is now wrapped in a `try/catch` for `IllegalArgumentException` and logs a message instead of throwing. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a14cdad. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1202552961248957/task/1213449998545088?focus=true ### Description Removes the temporary Block Store de-risking capability observer now that sufficient production data has been collected. - Deleted `SyncAutoRecoveryCapabilityObserver` and its tests - Removed 9 Block Store daily pixels from `SyncPixels.kt` and `SyncPixelParamRemovalPlugin.kt` - Removed `syncAutoRecoveryCapabilityDetectionWrite` and `syncAutoRecoveryCapabilityDetectionRead` feature flags from `SyncFeature.kt` ### Steps to test this PR **QA optional** - [ ] [Optional] Build and verify app launches without crashes - [ ] [Optional] Confirm no Block Store capability pixels are fired <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk cleanup that removes de-risking telemetry and its feature flags/tests; behavior change is limited to no longer performing Block Store capability checks or emitting related daily pixels. > > **Overview** > Removes the temporary Block Store de-risking path by deleting `SyncAutoRecoveryCapabilityObserver` (and its test) that ran capability read/write checks after privacy config downloads. > > Cleans up associated telemetry by dropping the Block Store daily pixel definitions and their parameter-removal entries, and removes the two remote feature toggles (`syncAutoRecoveryCapabilityDetectionWrite`/`Read`) used to gate this logic. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0b5823f. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211724162604201/task/1213684338936124?focus=true ### Description Added a new lint rule that prevents the use of `postValue()` on `SingleLiveEvent` instances. The rule enforces the use of `setValue()` instead to avoid silently dropping commands when multiple `postValue()` calls occur before the main thread processes them. The detector identifies calls to `postValue()` on `SingleLiveEvent` or its subclasses and reports an error with guidance to use `setValue()` on the main thread or wrap background thread calls with `withContext(dispatchers.main())`. ### Steps to test this PR _Lint Rule Validation_ - [ ] Create a class with a `SingleLiveEvent` property and call `postValue()` on it - verify lint error appears - [ ] Change the same call to use `setValue()` or `.value = ...` - verify lint error disappears - [ ] Call `postValue()` on a regular `MutableLiveData` instance - verify no lint error occurs ### UI changes No UI changes. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes how several UI `command` emissions are dispatched (switching to main-thread `setValue`), which can affect timing/order of one-shot navigation/dialog events if any callers relied on background posting. > > **Overview** > Adds a new lint rule (`NoPostValueOnSingleLiveEventDetector`) registered in the project’s lint registry (with unit tests) to **error** on `SingleLiveEvent.postValue()` usage, guiding developers to use main-thread `setValue`/`.value = ...` to avoid dropped commands. > > Updates multiple ViewModels (notably `BrowserTabViewModel`, plus `FeedbackViewModel` and `BookmarksViewModel`) to replace `postValue` with direct `.value` assignments, and wraps previously background-thread emissions in `dispatchers.main()` launches where needed. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit bc1b973. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1211766481496464?focus=true ### Description Adds in the headless sync setup using the persisted sync recovery key. This is still gated by the `syncAutoRestore` feature flag which **remains disabled** for now (you'll need to update this manually for some of the tests) ### Steps to test this PR **Notes** - Prerequisites: Internal build installed on a device with Google Play Services, logged into Google account. - Navigate to Settings > Sync Dev Settings to access Block Store controls. - ℹ️ Clearing data and relaunching the app does not restore Block Store data; it has to be an uninstall/reinstall. - Suggested logcat filter: `Sync-Recovery|Sync-AutoRestore` **Scenario 1: Feature flag OFF (default) — no restore offered regardless of stored key** - [x] Fresh install - [x] Launch app and verify the "Restore" dialog is not shown, normal onboarding flow proceeds - [x] go to Sync Dev Settings and write any string to Block Store - [x] Uninstall and reinstall (do not clear app data) - [x] Launch app and verify the "Restore" dialog is still not shown (because flag is off) **Scenario 2: Feature flag ON, no recovery key — no restore offered** - [x] Apply the patch defined below to hardcode `syncAutoRestore` to be enabled, and install - [x] Clear app data to ensure Block Store has no data - [x] Launch app — verify the "Restore" dialog is not shown, normal onboarding proceeds **Scenario 3: Feature flag ON, recovery key present — user accepts restore** - [x] Keep the hardcoded FF enabled changes from before - [x] Save a password or two - [x] Set up Sync on the device, using Sync & Backup -> Sync & Back Up This Device and copy the recovery key when it's available using the `Copy code` button - [x] Paste the recovery key into Block Store via Sync Dev Settings and use the `Write` button to persist it - [x] Uninstall and reinstall (do not clear app data) - [x] Go through onboarding — verify the "Restore" dialog is shown - [x] Tap "Restore My Stuff" — verify onboarding continues normally (e.g., comparison chart shown next) - [x] Complete onboarding, then go to Settings > Sync — verify sync account is re-established - [x] Verify previous password is available **Scenario 4: Feature flag ON, recovery key present — user skips restore** - [x] Repeat setup from Scenario 3 (to get a valid recovery code in Block Store, uninstall and then reinstall) - [x] Launch app — verify the "Restore" dialog is shown - [x] Tap "Skip" button from that dialog — verify you see the `Got it! I'll skip other tips` dialog - [x] Complete onboarding, then go to Settings > Sync — verify sync is not set up **Scenario 5: Invalid code persisted** - [x] Use `Sync Dev Settings` to write an invalid recovery code (e.g., a few random characters) - [x] Uninstall and reinstall (do not clear app data) - [x] Launch app — verify the "Restore" dialog is shown - [x] Tap `Restore My Stuff`. Verify the UX continues normally and in logs you see `Sync-Recovery: restore failed` ## Patch to enable FF for `syncAutoRestore` ``` Index: sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt (revision Staged) +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt (date 1773231063963) @@ -78,6 +78,6 @@ @Toggle.DefaultValue(DefaultFeatureValue.TRUE) fun syncAutoRecoveryCapabilityDetectionRead(): Toggle - @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL) fun syncAutoRestore(): Toggle } ```
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1213646602671393?focus=true ### Description When auto-restoring a sync account from Block Store on reinstall, the previous implementation called `login()` without providing the original device ID, causing the sync backend to assign a new device ID. This left the old device entry as an orphan on the account — visible to other connected devices as a ghost entry. This PR fixes the orphaned device problem by storing the device ID alongside the recovery code in Block Store as a JSON payload, then reusing that device ID during auto-restore login. This matches the approach iOS already uses. No user-facing prod changes as it is still guarded by `syncAutoRestore` FF which remains `DISABLED` ### Steps to test this PR Logcat filter `message~:"Sync-Recovery|Sync-AutoRestore"` **Pre-requsites** 1. Device/emulator with Google Play Services 2. Be signed in to the Google account on your device, and have device-level backups enabled 3. Have device-level auth set (PIN/Pattern/Password) ### Feature flag disabled (default) - [x] Fresh install `internalDebug`, launch app - [x] Verify in logs, `Sync-AutoRestore: canRestore=false` ### Feature flag enabled ❗ **hardcode the feature flag to enabled for the following tests** **Testing recovery code cleared when logging out of sync** - [x] Apply patch below - [x] Install `internalDebug` and launch - [x] Verify `canRestore=false` in logs - [x] Verify you do **not** see "Restore my stuff" dialog - [x] Set up sync `Sync and Back Up This Device` then disable sync again - [x] Verify in logs, `sync disabled, clearing recovery code from Block Store` **Ensuring device not orphaned on restore** - [x] Set up sync again, and this time copy the recovery code using the `Copy Code` button - [x] Visit `Sync Dev Settings`, and paste into the `Recovery code` edit text (in the `Persistent storage` section) - [x] Scroll down to the `Account Settings` section, and tap on `Device id`'s value - [x] Paste into the `Device ID` edit text - [x] Tap the `Write` button and verify you see the `Stored successfully` toast and JSON containing both recovery and device ID is shown - [x] Uninstall and reinstall (do not use clear app data). Launch app - [x] Verify you see in logs, `canRestore=true` - [x] Verify you are offered to `Restore My Stuff`. Tap that button. - [x] Skip rest of onboarding, and visit `Settings -> Sync & Backup` - [x] Verify sync is set up, and that there is only one device showing ### Patch to enabled `syncAutoRestore` feature flag ``` Index: sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt (revision Staged) +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt (date 1773659914685) @@ -78,6 +78,6 @@ @Toggle.DefaultValue(DefaultFeatureValue.TRUE) fun syncAutoRecoveryCapabilityDetectionRead(): Toggle - @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL) fun syncAutoRestore(): Toggle } ``` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes sync auto-restore login behavior to reuse a stored device ID and adds new persistence/cleanup logic; mistakes could cause failed restores or unexpected sign-in/device registration behavior. > > **Overview** > Fixes sync auto-restore creating *orphaned/ghost devices* by allowing `processCode`/recovery `login` to accept an `existingDeviceId` and using it during restore instead of always generating a new one. > > Introduces `SyncAutoRestoreManager` to persist a JSON payload (`recovery_code` + optional `device_id`) in Block Store, updates `RealSyncAutoRestore` to read that payload (and to hard-skip when the `syncAutoRestore` flag is off), and adds a lifecycle observer that clears the stored recovery payload when the user signs out while auto-restore is enabled. > > Updates internal sync settings UI to write/read the new payload format (separate recovery-code and device-id inputs, plus tap-to-copy fields) and adjusts/extends unit tests to cover the new manager, observer, and updated auto-restore flow. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit fcb4307. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213614217074373?focus=true ### Description Enables Develocity PTS for JVM unit tests for local builds and PR checks. Keeps it disabled for post-merge checks and nightly flows since we want to run the full suite. This runs with the [Standard profile](https://docs.gradle.com/develocity/current/using-develocity/predictive-test-selection/#selection-profiles) that balances speed and selecting relevant tests. Alternatively, we can go with Conservative to select more tests but reduces time savings. ### Steps to test this PR _Run tests locally_ - [x] Run `./gradlew :pir-impl:testDebugUnitTest -Dpts.enabled=false` (replace `pir-impl` with any module you are familiar with the unit tests) to get a baseline time of how long it takes. It should run all tests in that module - [x] Run `./gradlew :pir-impl:testDebugUnitTest` twice in a row. The second time it should finish in less than a second and run no tests. - [ ] Now change some code in that module in a way that would break the test. For example in `pir-impl`, edit `PirAuthInterceptor:65` and change `bearer` to `Bearer`. This change will break one of the tests that checks for correct header value. - [ ] Run `./gradlew :pir-impl:testDebugUnitTest` again. You should see something like `Predictive Test Selection: 4 of 74 test classes selected with profile 'Standard' (saving 54.371s serial time)` and the tests that were run should fail. ### UI changes No UI change <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the JVM unit-test execution model in CI by enabling Develocity Predictive Test Selection by default, which can unintentionally skip relevant tests if misconfigured. Adds JUnit Platform/Vintage dependencies across modules, which may change how tests are discovered and run. > > **Overview** > **Enables Develocity Predictive Test Selection (PTS) for JVM unit tests** by configuring all Gradle `Test` tasks to `useJUnitPlatform()` and wiring `develocity.predictiveTestSelection.enabled` to a `-Dpts.enabled` system property (defaulting to `true`). > > CI workflows now **disable PTS for post-merge/nightly and external reference test runs** by passing `-Dpts.enabled=false`, while PR checks keep PTS enabled by default. This also adds `org.junit.vintage:junit-vintage-engine` (and its version pin) to test dependencies so existing JUnit 4 tests continue to run under the JUnit Platform. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e0dfd38. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213634152051160?focus=true ### Description See attached description ### Steps to test this PR https://app.asana.com/1/137249556945/task/1213721083788440?focus=true <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a large set of new bundled broker configuration JSONs that directly drive PIR scan/opt-out automation; incorrect selectors/flows or `removedAt` flags could cause broken runs or unintended broker enablement/disablement. > > **Overview** > Adds a batch of new broker JSON assets under `pir-impl/src/main/assets/brokers/`, expanding the set of bundled broker definitions used for PIR scanning and opt-out flows. > > These configs include new `scan`/`optOut` step recipes (including captcha handling and email confirmations), parent/mirror-site relationships, scheduling parameters, and per-broker activation state via `removedAt` (some brokers ship pre-removed). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5b9dc42. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1213433888294716?focus=true ### Description - Adds the native input to the Duck.ai contextual sheet ### Steps to test this PR - [ ] Enable the native input - [ ] Open contextual Duck.ai and send prompt - [ ] Verify that the native input is visible - [ ] Send a prompt - [ ] Verify that the prompt is submitted - [ ] Change to search - [ ] Submit a query - [ ] Very that contextual is closed and the search is submitted ### UI changes | Before | After | | ------ | ----- | <img width="1280" height="2856" alt="Screenshot_20260317_004850" src="https://github.com/user-attachments/assets/e20c0ffc-de95-4ea8-98e3-36b65285c59c" />|<img width="1280" height="2856" alt="Screenshot_20260317_010044" src="https://github.com/user-attachments/assets/2682325c-72b6-4617-8668-bbe9a6e0ad44" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces a new native input overlay that sends JS subscription events into the contextual WebView and changes sheet-mode rendering/visibility, so regressions could impact prompt submission or UI state toggling. > > **Overview** > Adds an optional **native input overlay** to the Duck.ai contextual sheet when in WebView mode, driven by a new `ContextualNativeInputManager` that wires up `NativeInputModeWidget` and toggles visibility based on the user setting. > > Native chat prompts are now submitted via `JsMessaging` subscription events (`submitAIChatNativePrompt` / `submitPromptInterruption`), while search submissions close the contextual sheet and open the query in a new browser tab. The layout is updated to wrap the `WebView` in a container and overlay the new input card at the bottom. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 31a9ea3. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213310129933666?focus=true ### Description - Adds a Duck.ai dev setting to override the Duck.ai URL - Centralizes all uses of the “duck.ai” domain - Changes the default `DUCK_CHAT_WEB_LINK` to "https://duck.ai/chat?duckai=5” (the same URL that’s used in the config) ### Steps to test this PR - [ ] Go to Settings > Developer Settings > Custom Duck.ai URL - [ ] Enter a custom URL - [ ] Tap “Save" - [ ] Verify that the app is restarted - [ ] Go to Duck.ai - [ ] Verify that the custom URL is used ### UI changes | Developer Settings | Custom Duck.ai URL | | ------ | ----- | <img width="1080" height="2340" alt="Screenshot_20260312_212642" src="https://github.com/user-attachments/assets/fff528da-6a7d-40ef-8fbc-639c71283547" />|<img width="1080" height="2340" alt="Screenshot_20260312_213453" src="https://github.com/user-attachments/assets/32ff1142-a813-4544-9db3-41d287f31730" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches multiple privacy/cookie-clearing and JS messaging allowlist paths by replacing hardcoded `duck.ai` checks with an injected host provider, so a misconfiguration could break Duck.ai navigation, data clearing, or messaging on that domain. > > **Overview** > Adds a `DuckAiHostProvider` abstraction and rewires Duck.ai-related logic to use it instead of hardcoded `duck.ai` (cookies/third‑party cookie exceptions, IndexedDB/site data clearing exclusions, site permissions microphone recovery allowlist, and multiple JS messaging `allowedDomains` lists). > > Introduces an *internal/dev-only* `duckchat-internal` module and Developer Settings UI to override the Duck.ai URL, persisting it in a small data store and replacing the default provider via DI; the DuckChat entry URL is also updated to the direct `https://duck.ai/chat?duckai=5` form. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4a3a624. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1213630582405252 ### Description Adds a `FirstScreenHandlerImpl` that decides the first screen shown when the app is opened, with two modes controlled by the `showNTPAfterIdleReturn` feature flag: **When `showNTPAfterIdleReturn` is enabled** (new behavior): - On every app open (fresh launch or return from background), checks if the idle timeout has elapsed since the app was last backgrounded. - If the timeout has passed (or no previous timestamp exists), delegates to `ShowOnAppLaunchOptionHandler` to show the configured launch screen. - If the timeout hasn't passed, does nothing (user returns to their previous state). - Timeout is configurable via remote settings JSON (`timeoutMinutes` field), defaults to 30 minutes. **When `showNTPAfterIdleReturn` is disabled** (legacy behavior): - Only on fresh launches: delegates to `ShowOnAppLaunchOptionHandler` if `showOnAppLaunchFeature` is enabled. - Non-fresh launches (return from background): does nothing. Key changes: - New `FirstScreenHandlerImpl` registered as a `BrowserLifecycleObserver` via `@ContributesMultibinding` - `showNTPAfterIdleReturn` is checked first and takes precedence over `showOnAppLaunchFeature` - Records background timestamp on `onClose()` using `System.currentTimeMillis()` (survives reboots) - `BrowserViewModel` no longer owns show-on-app-launch logic — fully decoupled - New `lastSessionBackgroundTimestamp` in `SettingsDataStore` (separate from `AutomaticDataClearer`'s timestamp) - `showNTPAfterIdleReturn` feature flag defaults to `FALSE`, enabled on internal builds via `@InternalAlwaysEnabled` ### Steps to test this PR _Idle return enabled — timeout exceeded (cold start)_ - [x] Enable `showNTPAfterIdleReturn` feature flag (auto-enabled on internal builds) - [x] Open the app and navigate to a website - [x] Background the app and wait longer than the configured timeout (default 1 min on internal) - [x] Reopen the app — the configured ShowOnAppLaunch option should be applied _Idle return enabled — timeout exceeded (fresh launch)_ - [x] Enable `showNTPAfterIdleReturn` feature flag - [x] Open the app and navigate to a website - [x] Force-stop the app and wait longer than the configured timeout - [x] Reopen the app — the configured ShowOnAppLaunch option should be applied _Idle return enabled — timeout not exceeded_ - [x] Enable `showNTPAfterIdleReturn` feature flag - [x] Open the app and navigate to a website - [x] Background the app and reopen within the timeout window - [x] The previous tab should still be visible (no action taken) _Idle return enabled — first ever launch_ - [ ] Enable `showNTPAfterIdleReturn` feature flag - [ ] Clear app data or fresh install - [ ] Open the app — the configured ShowOnAppLaunch option should be applied (no previous timestamp) _Idle return disabled — fresh launch (legacy behavior)_ - [x] Disable `showNTPAfterIdleReturn` feature flag - [x] Enable `showOnAppLaunchFeature` and set the option to "New Tab Page" - [x] Force-stop the app and reopen — a new tab page should be shown - [x] Set the option to "Specific Page" with a URL, force-stop and reopen — the specific page should load - [x] Set the option to "Last Opened Tab", force-stop and reopen — the last opened tab should be shown _Idle return disabled — non-fresh launch (legacy behavior)_ - [x] Disable `showNTPAfterIdleReturn` feature flag - [x] Open the app, navigate to a website, background it for any duration - [x] Reopen the app — the previous tab should still be visible (no action taken) _Idle return disabled — ShowOnAppLaunch also disabled_ - [x] Disable both `showNTPAfterIdleReturn` and `showOnAppLaunchFeature` - [x] Force-stop and reopen the app — default behavior, no delegation ### UI changes No UI changes — this is a behavioral change only. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes app-open/return behavior by moving “show on app launch” decisions into a new lifecycle observer and gating it behind remote config + persisted timestamps, which could affect what users see when resuming the app. > > **Overview** > Adds `FirstScreenHandlerImpl` as a `BrowserLifecycleObserver` to centralize first-screen selection on app open. When remote flag `androidBrowserConfig.showNTPAfterIdleReturn` is enabled, it conditionally applies `ShowOnAppLaunchOptionHandler` based on elapsed time since the app was last backgrounded (remote-configurable `timeoutMinutes`, default 30m); otherwise it preserves the legacy *fresh-launch only* behavior. > > Removes the previous “show on app launch” handling from `BrowserActivity`/`BrowserViewModel`, introduces persisted `SettingsDataStore.lastSessionBackgroundTimestamp`, adds the new remote sub-toggle to `AndroidBrowserConfigFeature`, and updates/adds tests accordingly. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2d06fa7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot]
Can you help keep this open source service alive? 💖 Please sponsor : )